package xyz.lwm.lazycat.utility.convert;

import xyz.lwm.lazycat.utility.Assert;
import xyz.lwm.lazycat.utility.It;
import xyz.lwm.lazycat.utility.StrPool;
import xyz.lwm.lazycat.utility.StringUtil;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.sql.Time;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * converter implement instances
 *
 * @author lwm
 */
public abstract class Converters {

    /**
     * <h3>get converter</h3>
     *
     * @param type type to convert
     * @param <T>  generic type
     * @return converter
     */
    public static <T> Converter<T> getConverter(Class<T> type) {
        return getConverter(type, k -> new UnOperationConverter<>());
    }

    /**
     * <h3>get converter</h3>
     *
     * @param type             type to convert
     * @param defaultConverter default converter
     * @param <T>              generic type
     * @return converter
     */
    @SuppressWarnings("unchecked")
    public static <T> Converter<T> getConverter(Class<T> type, Function<Class<T>, Converter<T>> defaultConverter) {
        return type.isEnum() ? (Converter<T>) new EnumConverter((Class<Enum<?>>) type) :
                (Converter<T>) converters.getOrDefault(type, defaultConverter.apply(type));
    }

    /**
     * <h3>register converter</h3>
     * <p>we can register new converter to extend function</p>
     *
     * @param type      type to convert
     * @param converter converter instance to register
     * @param overwrite if true register converter even if already registered
     */
    public static void register(Class<?> type, Converter<?> converter, boolean overwrite) {
        if (!overwrite) {
            Assert.isTrue(!converters.containsKey(type), "Converter already registered for type: " + type);
        }
        register(type, converter);
    }

    private static void register(Class<?> type, Converter<?> converter) {
        converters.put(type, converter);
    }

    private static class IntegerConverter implements Converter<Integer> {

        private final boolean boxed;

        private IntegerConverter(boolean boxed) {
            this.boxed = boxed;
        }

        @Override
        public Integer fromString(String value) {
            return value != null ? Integer.valueOf(value) : (boxed ? null : 0);
        }
    }

    private static class LongConverter implements Converter<Long> {

        private final boolean boxed;

        private LongConverter(boolean boxed) {
            this.boxed = boxed;
        }

        @Override
        public Long fromString(String value) {
            return value != null ? Long.valueOf(value) : (boxed ? null : 0L);
        }
    }

    private static class ShortConverter implements Converter<Short> {

        private final boolean boxed;

        private ShortConverter(boolean boxed) {
            this.boxed = boxed;
        }

        @Override
        public Short fromString(String value) {
            return value != null ? Short.valueOf(value) : (boxed ? null : (short) 0);
        }
    }

    private static class ByteConverter implements Converter<Byte> {

        private final boolean boxed;

        private ByteConverter(boolean boxed) {
            this.boxed = boxed;
        }

        @Override
        public Byte fromString(String value) {
            return value != null ? Byte.valueOf(value) : (boxed ? null : (byte) 0);
        }
    }

    private static class FloatConverter implements Converter<Float> {

        private final boolean boxed;

        private FloatConverter(boolean boxed) {
            this.boxed = boxed;
        }

        @Override
        public Float fromString(String value) {
            return value != null ? Float.valueOf(value) : (boxed ? null : 0.0f);
        }
    }

    private static class DoubleConverter implements Converter<Double> {

        private final boolean boxed;

        private DoubleConverter(boolean boxed) {
            this.boxed = boxed;
        }

        @Override
        public Double fromString(String value) {
            return value != null ? Double.valueOf(value) : (boxed ? null : 0.0);
        }
    }

    private static class BooleanConverter implements Converter<Boolean> {

        private final boolean boxed;

        private BooleanConverter(boolean boxed) {
            this.boxed = boxed;
        }

        @Override
        public Boolean fromString(String value) {
            if (value == null) {
                return boxed ? null : false;
            }
            switch (value.toLowerCase()) {
                case "true":
                case "1":
                case "yes":
                case "y":
                case "on":
                    return Boolean.TRUE;
                case "false":
                case "0":
                case "no":
                case "n":
                case "off":
                    return Boolean.FALSE;
                default:
                    throw new IllegalArgumentException("Invalid boolean value: " + value);
            }
        }
    }

    private static class CharacterConverter implements Converter<Character> {

        private final boolean boxed;

        private CharacterConverter(boolean boxed) {
            this.boxed = boxed;
        }

        @Override
        public Character fromString(String value) {
            return value != null ? Character.valueOf(value.charAt(0)) : (boxed ? null : '\0');
        }
    }

    private static class StringConverter implements Converter<String> {
        @Override
        public String fromString(String value) {
            return value;
        }
    }

    private static class StringBuilderConverter implements Converter<StringBuilder> {
        @Override
        public StringBuilder fromString(String value) {
            return value != null ? new StringBuilder(value) : null;
        }
    }

    private static class StringBufferConverter implements Converter<StringBuffer> {
        @Override
        public StringBuffer fromString(String value) {
            return value != null ? new StringBuffer(value) : null;
        }
    }

    private static class BigIntegerConverter implements Converter<BigInteger> {
        @Override
        public BigInteger fromString(String value) {
            return value != null ? new BigInteger(value) : null;
        }
    }

    private static class BigDecimalConverter implements Converter<BigDecimal> {
        @Override
        public BigDecimal fromString(String value) {
            return value != null ? new BigDecimal(value) : null;
        }
    }

    private static class UtilDateConverter implements Converter<Date> {

        private final DateFormat fmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");

        @Override
        public String toString(Date value) {
            return value == null ? null : fmt.format(value);
        }

        @Override
        public Date fromString(String value) {
            return value != null ? java.sql.Timestamp.valueOf(value) : null;
        }

    }

    private static class SqlDateConverter implements Converter<java.sql.Date> {

        private final DateFormat fmt = new SimpleDateFormat("yyyy-MM-dd");

        @Override
        public String toString(java.sql.Date value) {
            return value == null ? null : fmt.format(value);
        }

        @Override
        public java.sql.Date fromString(String value) {
            return value != null ? java.sql.Date.valueOf(value) : null;
        }

    }

    private static class SqlTimeConverter implements Converter<Time> {

        private final DateFormat fmt = new SimpleDateFormat("HH:mm:ss.SSS");

        @Override
        public String toString(Time value) {
            return value == null ? null : fmt.format(value);
        }

        @Override
        public Time fromString(String value) {
            return value != null ? new Time(java.sql.Timestamp.valueOf("2000-01-01 " + value).getTime()) : null;
        }

    }

    private static class SqlTimestampConverter implements Converter<java.sql.Timestamp> {

        private final DateFormat fmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");

        @Override
        public String toString(java.sql.Timestamp value) {
            return value == null ? null : fmt.format(value);
        }

        @Override
        public java.sql.Timestamp fromString(String value) {
            return value != null ? java.sql.Timestamp.valueOf(value) : null;
        }

    }

    private static class LocalDateTimeConverter implements Converter<LocalDateTime> {

        private final DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");

        @Override
        public String toString(LocalDateTime value) {
            return value == null ? null : value.format(fmt);
        }

        @Override
        public LocalDateTime fromString(String value) {
            return value != null ? java.sql.Timestamp.valueOf(value).toLocalDateTime() : null;
        }

    }

    private static class LocalDateConverter implements Converter<LocalDate> {

        private final DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd");

        @Override
        public String toString(LocalDate value) {
            return value == null ? null : value.format(fmt);
        }

        @Override
        public LocalDate fromString(String value) {
            return value != null ? getConverter(java.sql.Date.class).fromString(value).toLocalDate() : null;
        }

    }

    private static class LocalTimeConverter implements Converter<LocalTime> {

        private final DateTimeFormatter fmt = DateTimeFormatter.ofPattern("HH:mm:ss.SSS");

        @Override
        public String toString(LocalTime value) {
            return value == null ? null : value.format(fmt);
        }

        @Override
        public LocalTime fromString(String value) {
            return value != null ? getConverter(Time.class).fromString(value).toLocalTime() : null;
        }

    }

    private static class DurationConverter implements Converter<Duration> {

        private final Pattern pattern = Pattern.compile("^([+-]?\\d+)([a-zA-Z]{0,2})$");

        private enum Unit {

            NANOS(ChronoUnit.NANOS, "ns", Duration::toNanos),
            MICROS(ChronoUnit.MICROS, "us", (duration) -> duration.toMillis() * 1000L),
            MILLIS(ChronoUnit.MILLIS, "ms", Duration::toMillis),
            SECONDS(ChronoUnit.SECONDS, "s", Duration::getSeconds),
            MINUTES(ChronoUnit.MINUTES, "m", Duration::toMinutes),
            HOURS(ChronoUnit.HOURS, "h", Duration::toHours),
            DAYS(ChronoUnit.DAYS, "d", Duration::toDays);

            private final ChronoUnit chronoUnit;

            private final String suffix;

            private final Function<Duration, Long> longValue;

            Unit(ChronoUnit chronoUnit, String suffix, Function<Duration, Long> toUnit) {
                this.chronoUnit = chronoUnit;
                this.suffix = suffix;
                this.longValue = toUnit;
            }

            public Duration parse(String value) {
                return Duration.of(Long.parseLong(value), this.chronoUnit);
            }

            public String print(Duration value) {
                return longValue(value) + this.suffix;
            }

            public long longValue(Duration value) {
                return this.longValue.apply(value);
            }

            public static Unit fromChronoUnit(ChronoUnit chronoUnit) {
                if (chronoUnit == null) {
                    return MILLIS;
                }
                for (Unit candidate : values()) {
                    if (candidate.chronoUnit == chronoUnit) {
                        return candidate;
                    }
                }
                throw new IllegalArgumentException("Unknown unit " + chronoUnit);
            }

            public static Unit fromSuffix(String suffix) {
                for (Unit candidate : values()) {
                    if (candidate.suffix.equalsIgnoreCase(suffix)) {
                        return candidate;
                    }
                }
                throw new IllegalArgumentException("Unknown unit '" + suffix + "'");
            }

        }

        @Override
        public String toString(Duration value) {
            if (value == null) {
                return null;
            }
            long millis = value.toMillis();
            if (millis % 1000 != 0 || millis < 1000) {
                return Unit.MILLIS.print(value);
            }
            long seconds = value.getSeconds();
            if (seconds % 60 != 0 || seconds < 60) {
                return Unit.SECONDS.print(value);
            }
            long minutes = value.get(ChronoUnit.MINUTES);
            if (minutes % 60 != 0 || minutes < 60) {
                return Unit.MINUTES.print(value);
            }
            long hours = value.get(ChronoUnit.HOURS);
            if (hours % 24 != 0 || hours < 24) {
                return Unit.HOURS.print(value);
            }
            return Unit.DAYS.print(value);
        }

        @Override
        public Duration fromString(String value) {
            if (value == null) {
                return null;
            }
            Matcher matcher = pattern.matcher(value);
            Assert.isTrue(matcher.matches(), "Does not match simple duration pattern");
            String suffix = matcher.group(2);
            return (StringUtil.isNotEmpty(suffix) ? Unit.fromSuffix(suffix) : Unit.fromChronoUnit(null))
                    .parse(matcher.group(1));
        }

    }

    private static class CalendarConverter implements Converter<Calendar> {

        @Override
        public String toString(Calendar value) {
            return getConverter(Date.class).toString(value.getTime());
        }

        @Override
        public Calendar fromString(String value) {
            return It.of(value).let(v -> {
                Date date = getConverter(Date.class).fromString(value);
                Calendar calendar = Calendar.getInstance();
                calendar.setTime(date);
                return calendar;
            }).getOr(null);
        }

    }

    private static class ByteArrayConverter implements Converter<byte[]> {

        @Override
        public String toString(byte[] value) {
            return value == null ? null : new String(value, StandardCharsets.UTF_8);
        }

        @Override
        public byte[] fromString(String value) {
            return value != null ? value.getBytes(StandardCharsets.UTF_8) : null;
        }

    }

    private static class EnumConverter implements Converter<Enum<?>> {

        private final Class<Enum<?>> enumType;

        private EnumConverter(Class<Enum<?>> enumType) {
            this.enumType = enumType;
        }

        @Override
        public Enum<?> fromString(String value) {
            for (Enum<?> e : enumType.getEnumConstants()) {
                if (e.name().equalsIgnoreCase(value)) {
                    return e;
                }
            }
            return null;
        }
    }

    private static class UnOperationConverter<T> implements Converter<T> {
        @Override
        public T fromString(String value) {
            String clzNames = StringUtil.join(new ArrayList<>(converters.keySet()), StrPool.STR_COMMA,
                    StrPool.STR_LEFT_SQUARE_BRACKET, StrPool.STR_RIGHT_SQUARE_BRACKET, Class::getName);
            throw new UnsupportedOperationException("类型转换不支持, 支持类型为 " + clzNames);
        }
    }

    private static final Map<Class<?>, Converter<?>> converters = new ConcurrentHashMap<>();

    static {
        register(Integer.class, new IntegerConverter(true));
        register(int.class, new IntegerConverter(false));
        register(Long.class, new LongConverter(true));
        register(long.class, new LongConverter(false));
        register(Short.class, new ShortConverter(true));
        register(short.class, new ShortConverter(false));
        register(Byte.class, new ByteConverter(true));
        register(byte.class, new ByteConverter(false));
        register(Float.class, new FloatConverter(true));
        register(float.class, new FloatConverter(false));
        register(Double.class, new DoubleConverter(true));
        register(double.class, new DoubleConverter(false));
        register(Boolean.class, new BooleanConverter(true));
        register(boolean.class, new BooleanConverter(false));
        register(Character.class, new CharacterConverter(true));
        register(char.class, new CharacterConverter(false));
        register(String.class, new StringConverter());
        register(StringBuilder.class, new StringBuilderConverter());
        register(StringBuffer.class, new StringBufferConverter());
        register(BigInteger.class, new BigIntegerConverter());
        register(BigDecimal.class, new BigDecimalConverter());
        register(Date.class, new UtilDateConverter());
        register(java.sql.Date.class, new SqlDateConverter());
        register(Time.class, new SqlTimeConverter());
        register(java.sql.Timestamp.class, new SqlTimestampConverter());
        register(LocalDateTime.class, new LocalDateTimeConverter());
        register(LocalDate.class, new LocalDateConverter());
        register(LocalTime.class, new LocalTimeConverter());
        register(Duration.class, new DurationConverter());
        register(Calendar.class, new CalendarConverter());
        register(byte[].class, new ByteArrayConverter());
    }

}
